package com.yifeng.repo.base.security.auth.impl;

import com.gomcarter.frameworks.base.exception.CustomException;
import com.gomcarter.frameworks.base.json.ErrorCode;
import com.google.common.base.Strings;
import com.nimbusds.jose.jwk.KeyType;
import com.yifeng.repo.base.constant.BaseConstant;
import com.yifeng.repo.base.dto.UserDto;
import com.yifeng.repo.base.security.auth.SecurityChecker;
import com.yifeng.repo.base.security.auth.constant.SecurityConstant;
import com.yifeng.repo.base.security.auth.exception.SecurityCheckException;
import com.yifeng.repo.base.utils.codec.JwsHelper;
import com.yifeng.repo.base.utils.converter.JacksonHelper;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.security.interfaces.ECPublicKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by daibing on 2024/2/28.
 */
@Slf4j
public abstract class AbstractSecurityChecker implements SecurityChecker {

    private static final Long ONE_HOUR_MILLIS = 60 * 60 * 1000L;
    private final ConcurrentMap<String, ECPublicKey> ecPublicKeyMap = new ConcurrentHashMap<>();
    private final AtomicLong lastRefreshTime = new AtomicLong(0L);

    @Override
    public String checkAndGetToken(ServletRequest request) {
        String token = getTokenByField(request, SecurityConstant.TOKEN, onlyGetTokenFromHeader());
        if (!Strings.isNullOrEmpty(token)) {
            return token;
        }
        String tokenCustomName = System.getProperty(SecurityConstant.TOKEN_CUSTOM_NAME);
        if (!Strings.isNullOrEmpty(tokenCustomName)) {
            return getTokenByField(request, tokenCustomName, onlyGetTokenFromHeader());
        }
        if (getTokenFromSession()) {
            HttpSession session = ((HttpServletRequest) request).getSession();
            return session == null ? null : (String) session.getAttribute(SecurityConstant.TOKEN);
        }
        return null;
    }

    private String getTokenByField(ServletRequest request, String fieldName, boolean onlyGetTokenFromHeader) {
        HttpServletRequest req = (HttpServletRequest) request;
        if (onlyGetTokenFromHeader) {
            return req.getHeader(fieldName);
        }
        if (BaseConstant.HTTP_GET_METHOD.equalsIgnoreCase(req.getMethod())) {
            String token = req.getParameter(fieldName);
            if (!Strings.isNullOrEmpty(token)) {
                return token;
            }
        }
        return req.getHeader(fieldName);
    }

    @Override
    public UserDto checkAndGetUser(ServletRequest request, String token) {
        // 1. 检查请求不带token：如果有账号信息就转换出账号信息，否则直接返回null
        if (Strings.isNullOrEmpty(token)) {
            String userCustomName = System.getProperty(SecurityConstant.USER_CUSTOM_NAME);
            if (Strings.isNullOrEmpty(userCustomName)) {
                return null;
            }
            String user = ((HttpServletRequest) request).getHeader(userCustomName);
            return Strings.isNullOrEmpty(user) ? null : JacksonHelper.toObj(user, UserDto.class);
        }

        // 2. 解析 token
        return getUserByToken(token);
    }

    @Override
    public UserDto getUserByToken(String token) {
        // 1. 判断token
        if (Strings.isNullOrEmpty(token)) {
            return null;
        }

        // 2. 检查请求带token：先刷新公钥（1小时一次）
        refreshPublicKey(ONE_HOUR_MILLIS);

        // 3. jws 解包 token
        String user = unpackToken(token);
        if (Strings.isNullOrEmpty(user)) {
            throw new SecurityCheckException(ErrorCode.securityTokenParseError, "Token解密失败!");
        }

        // 4. 解析用户对象
        UserDto userDto = JacksonHelper.toObj(user, UserDto.class);
        if (userDto.isExpired()) {
            log.warn("Token已经过期：{}", token);
            throw new SecurityCheckException(ErrorCode.securityTokenExpiredError, "Token已经过期，请重新登录！");
        }
        return userDto;
    }

    private void refreshPublicKey(Long intervalMillis) {
        if (lastRefreshTime.get() + intervalMillis > System.currentTimeMillis()) {
            return;
        }
        synchronized (ecPublicKeyMap) {
            if (lastRefreshTime.get() + intervalMillis > System.currentTimeMillis()) {
                return;
            }
            try {
                List<String> newPublicKeys = listPublicKey();
                if (ecPublicKeyMap.keySet().containsAll(newPublicKeys) && newPublicKeys.containsAll(ecPublicKeyMap.keySet())) {
                    lastRefreshTime.set(System.currentTimeMillis());
                    return;
                }
                Map<String, ECPublicKey> map = new HashMap<>(newPublicKeys.size());
                for (String s : newPublicKeys) {
                    map.put(s, (ECPublicKey) JwsHelper.parsePublicKey(KeyType.EC, s));
                }
                ecPublicKeyMap.clear();
                ecPublicKeyMap.putAll(map);
                lastRefreshTime.set(System.currentTimeMillis());
            } catch (CustomException e) {
                // 调用相应服务端获取公钥失败，直接跳过。
                log.warn("获取公钥失败，error：", e);
            }
        }
    }

    private String unpackToken(String token) {
        for (Map.Entry<String, ECPublicKey> entry : ecPublicKeyMap.entrySet()) {
            try {
                return JwsHelper.unpackToken(token, entry.getValue());
            } catch (Throwable t) {
                log.warn("解密失败：token={}, publicKey={}", token, entry.getKey());
            }
        }
        return null;
    }

    /**
     * 具体实现类提供获取公钥的逻辑
     */
    protected abstract List<String> listPublicKey();

    /**
     * 是否仅从header获取token。
     */
    protected abstract boolean onlyGetTokenFromHeader();

    /**
     * 是否从session获取token。兼容R1项目
     */
    protected boolean getTokenFromSession() {
        return false;
    }
}
